package com.lecture.junit.tdd.example;


import javax.security.auth.login.AccountExpiredException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * There are several rules for logging in to our system:
 *
 * When logging in, a user provides a user name and password
 *
 * The user name is a non-blank, unique string containing just about any characters.
 * The password is a non-blank, unique string containing just about any characters.
 * ---------------------------------^ - OMG! You cannot use this password: John Doe already uses it.
 * If a user attempts to log in and provides a valid account but invalid password three times,
 *    their account is revoked
 *
 * However, if, for the same session, the user uses different account names,
 *    then the account will not be revoked (e.g. try to log in as yury1, yury2, yury1,
 *    three failed attempts but the user name is changed, no account gets revoked)
 *
 * Not existing user can not login
 *
 * A user cannot login to a revoked account
 *
 * If failing attempts are not consecutive then no Revoke on Account should be invoked
 *
 * A user must change their password every 30 days, so if they attempt to log in and the password is expired,
 *    then the user must change their password before they can successfully log in
 *
 * A user cannot use any of their previous 24 passwords
 *
 * User accounts have a time stamp of the last time the account was used
 *    (logged in, logged out, password changed). If an account has not been used for 45 days,
 *    then the account becomes revoked and can only be enabled by calling customer support.
 */

public class LoginService {

    public static final int MAX_RECENT_PASSWORDS = 24;
    private final IAccountRepository repo;

    private ConcurrentHashMap<String, LoginServiceState> statePerUser = new ConcurrentHashMap<>();

    public LoginService(IAccountRepository repo) {
        this.repo = repo;
    }

    public void login(String userName, String password) throws AccountNotFoundException, AccountAlreadyRevokedException, BlankLoginFieldException, MustChangePasswordException, AccountExpiredException {

        IAccount acc = getAccountIfNotBlank(userName, password);

        statePerUser.putIfAbsent(userName, new FirstFailedAttempt());
        statePerUser.get(userName).login(acc, password, this);
    }

    public void logout(String userName, String password) throws AccountNotFoundException, BlankLoginFieldException {

        IAccount acc = getAccountIfNotBlank(userName, password);
        acc.setLoggedIn(false);
        acc.updateLastActivityTimestamp();
    }

    public void setPassword(String userName, String oldPass, String newPass) throws AccountNotFoundException, BlankLoginFieldException, CannotUseRecentPasswordException {
        IAccount acc = getAccountIfNotBlank(userName, oldPass);

        if (!isBlank(newPass) && acc.passwordMatches(oldPass)) {
            if (acc.getRecentPasswords().contains(newPass)) {
                throw new CannotUseRecentPasswordException("This password is among last "
                        + LoginServiceState.NUM_LAST_PASSWORDS + " passwords. Please choose another.");
            }
            acc.updateLastActivityTimestamp();
            acc.updateLastPasswordSetTimestamp();
            if (acc.getRecentPasswords().size() == MAX_RECENT_PASSWORDS) {
                acc.forgetOldestPassword();
            }
            acc.getRecentPasswords().offer(newPass);
            acc.setPassword(newPass);
        }
    }

    public IAccount getAccountIfNotBlank(String userName, String password) throws BlankLoginFieldException, AccountNotFoundException {
        if (isBlank(userName) || isBlank(password)) {
            throw new BlankLoginFieldException("Account and pass cannot be blank.");
        }

        return getAccount(userName);
    }

    public IAccount getAccount(String userName) throws AccountNotFoundException {
        IAccount acc = repo.find(userName);

        if (acc == null) {
            throw new AccountNotFoundException("Account is not found");
        }
        return acc;
    }

    private boolean isBlank(String s) {
        return 0 == s.replaceAll("\\s", "").length();
    }

    public void setState(String userName, LoginServiceState state) {
        statePerUser.put(userName, state);
    }

}
